<template>
  <form :class="'ee-form layout-' + layout" v-if="modelValue && form">
    <div v-for="item in form" :key="item.key">
      <slot :name="(item.key) + '-field'" :field="item" :model="proxyModel" :value="proxyModel[item.key]">
        <label :for="item.key"
          :class="{ required: required && (item.required || item.rules?.some(i => i.required)), colon: colon }">
          {{ item.label || item.key }}
        </label>
        <div>
          <!-- 动态声明插槽，插槽名为字段名 -->
          <slot :name="item.key" :field="item" :model="proxyModel" :value="proxyModel[item.key]">
            <!-- 透传模板：将 [字段名]-[组件模板名] 透传到 [组件模板名] -->
            <component :is="item.component || 'ee-input'" v-bind="item.props" v-model="proxyModel[item.key]"
              :name="item.key" :placeholder="item.placeholder">
              <template v-for="slot in getSlots(item.key)" #[slot]="data">
                <slot :name="`${item.key}-${slot}`" v-bind="data"></slot>
              </template>
            </component>
          </slot>
          <p class="error" v-if="error[item.key]">
            {{ error[item.key] }}
          </p>
          <template v-else />
        </div>
      </slot>
    </div>
    <div>
      <label></label>
      <div>
        <slot>
          <ee-button color="primary" @click="submit">提交</ee-button>
          <ee-button v-if="onReset" color="primary" @click="reset">重置</ee-button><template v-else />
          <ee-button @click="cancel">取消</ee-button>
        </slot>
      </div>
    </div>
  </form>
</template>

<script type="text/javascript">
import parent from '../__mixin/relation/parent'
import { injectStore } from 'ee-vuex'
import EeButton from 'entry-engine-ui/src/ee-ui/basic/ee-button.vue';
import { reactive } from 'vue';

/** 表单字段规则
 * @template T
 * @typedef {object} rule
 * @property {boolean} [required] - 是否必填
 * @property {number} [min] - 数字，字符串为长度，选项则为选择项数
 * @property {number} [max] - 数字，字符串为长度，选项则为选择项数
 * @property {(value: T, model: any, field: field<T>) => string} [valid] - 自定义验证，传入字段值，返回错误信息。如果要需改字段值，可以通过 this.字段名 = value 修改
 * @property {string} [msg] - 规则验证失败时的错误提示，默认也有提示，可以通过 i18n 进行定义
 */

/** 表单字段
 * @template T
 * @typedef {object} field
 * @property {string} key - input 标签名字，也是表单字段名，嵌套字段可以 'a.b.c'
 * @property {string} [label] - 标签，不填则为 name
 * @property {{ get?: (obj) => T, set?: (obj, value: T) => void }} [value] - 需要读写的值，例如嵌套字段可以 'a.b.c'，也可以使用 computed 的形式自行设置 get/set
 * @property {boolean} [required] - 必填
 * @property {('text' | 'password' | 'email' | 'tel' | 'url' | 'textarea' | 'boolean' | 'number' | 'time' | 'week' | 'date' | 'datetime' | 'file' | 'radio' | 'checkbox' | 'range' | 'color' | 'reset' | 'submit' | 'select')} [type] - 字段类型，默认为text
 * @property {string} [placeholder] - 输入框默认显示的内容
 * @property {T | () => T} [default] - 默认值，也可以对 v-model 对象对应字段先赋值
 * @property {string | Component} [component] - 编辑字段使用自定义组件
 * @property {Object} [props] - 编辑组件绑定的 v-bind 对象
 * @property {rule<T>[]} [rules] - 填写规则
 */

/** 可用于 field.value 将 input 输入的字符串转换成数字
 * @param {string} key - 字段名
 * @param {number} [rate] - 转换一个比例，例如输入的是元转换成分，则比例为 100，默认为 1
 */
export function valueToNumber(key, rate = 1) {
  const VALUE = undefined;
  return {
    get(model) {
      return (Number(model[key]) / rate) || VALUE;
    },
    set(model, value) {
      model[key] = (Number(value) * rate || VALUE);
    }
  }
}

export default injectStore({
  name: "ee-form",
  components: { EeButton },
  mixins: [parent],
  data() {
    return {
      error: {},
      /** modelValue 的深拷贝临时对象，编辑时修改这个临时对象的值 */
      cloneModel: undefined,
    }
  },
  props: {
    modelValue: {
      init: {},
      set(value) {
        if (value) {
          if (this.cloneModel == value)
            return;
          // v-model 时，保持内外一致
          // :model-value 时，相当于外面给里面赋初始值
          this.cloneModel = JSON.parse(JSON.stringify(value));
          return this.cloneModel;
        }
        else
          this.cloneModel = undefined;
      },
    },
    form: {
      type: Object,
      /** @type {field<any>[]} */
      default: undefined,
      required: true,
    },
    /** 表单元素的布局方式
     * 
     * left - 标签左侧对齐
     * 
     * right - 标签右侧对齐(默认)
     * 
     * top - 标签上方左侧对齐
     * 
     * inline - 标签和表单项行内对齐，一般可以用于后台列表的筛选项
     */
    layout: {
      type: String,
      /** @type {('left' | 'right' | 'top' | 'inline')} */
      default: "right"
    },
    /** required 字段的 label 前增加'*' */
    required: Boolean,
    /** label 后增加':' */
    colon: Boolean,

    /** 提交事件 */
    onSubmit: Function,
    /** 重置事件 */
    onReset: [Boolean, Function],
    /** 取消事件 */
    onCancel: Function,
  },
  computed: {
    // todo: 将 proxyModel, getField, field 封装到一起
    /** cloneModel 的代理对象，对象可以通过例如 'a.b.c' 来访问对象的值 */
    proxyModel() {
      if (!this.cloneModel)
        return;
      const cache = {};
      const getProxy = (p) => {
        let proxy = cache[p];
        if (!proxy) {
          proxy = this.getField(p);
          cache[p] = proxy;
        }
        return proxy;
      };
      return new Proxy(this.cloneModel, {
        get: (target, p) => {
          const proxy = getProxy(p);
          if (proxy)
            return proxy.get(target);
          else
            return target[p];
        },
        set: (target, p, value) => {
          const field = this.form.find(i => i.key == p);
          // if (field?.type == 'number')
          //   value = Number(value);

          const proxy = getProxy(p);
          if (proxy)
            proxy.set(target, value);
          else
            target[p] = value;

          if (field && this.error[field.key]) {
            this.valid(field);
          }

          return true;
        }
      });
    },
  },
  methods: {
    /** 获取字段的读写函数
     * @param {string} key - 字段名
     */
    getField(key) {
      const field = this.form.find(i => i.key == key);
      if (field) {
        if (field.value && field.value.get && field.value.set)
          return field.value;

        let innerProxy;
        if (key.includes('.')) {
          const arr = key.split('.');
          innerProxy = {
            get(model) {
              let temp = model;
              for (let i = 0; i < arr.length; i++) {
                temp = temp[arr[i]];
                if (!temp)
                  return undefined;
              }
              return temp;
            },
            set(model, v) {
              let temp = model;
              for (let i = 0, e = arr.length - 1; i < e; i++) {
                if (!temp[arr[i]])
                  temp[arr[i]] = {};
                temp = temp[arr[i]];
              }
              temp[arr[arr.length - 1]] = v;
            }
          };
        }

        if (field.value) {
          if (!innerProxy)
            innerProxy = {};

          innerProxy.get = field.value.get || (model => model[key]);
          innerProxy.set = field.value.set || ((model, value) => model[key] = value);
        }
        return innerProxy;
      }
    },

    /** 获取透传给子组件内部的插槽
     * @param {string} key - 字段名
     */
    getSlots(key) {
      const prefix = key + '-';
      return Object.keys(this.$slots).filter(i => i.startsWith(prefix)).map(i => i.substring(prefix.length));
    },

    /** 验证一个字段
     * @param {field<any>} field
     * @returns {boolean} true:通过 / false:不通过
     */
    valid(field) {
      const model = this.proxyModel;
      if (field.rules) {
        const error = this.error;
        const value = model[field.key];
        const name = field.label || field.key;
        delete error[field.key];
        for (const rule of field.rules) {
          if (rule.required && !value) {
            error[field.key] = rule.msg || `请填写${name}`;
            continue;
          }

          if (rule.min != undefined) {
            if ((field.type == 'number' || value?.constructor == Number)) {
              if (value < rule.min) {
                error[field.key] = rule.msg || `${name}必须大于${rule.min}`;
                continue;
              }
            } else if (value?.constructor == String) {
              if (value?.length < rule.min) {
                error[field.key] = rule.msg || `${name}至少${rule.min}个字符`;
                continue;
              }
            } else if (value?.constructor == Array) {
              if (value?.length < rule.min) {
                error[field.key] = rule.msg || `${name}至少选择${rule.min}项`;
                continue;
              }
            }
          }

          if (rule.max != undefined) {
            if ((field.type == 'number' || value?.constructor == Number)) {
              if (value > rule.max) {
                error[field.key] = rule.msg || `${name}必须小于${rule.max}`;
                continue;
              }
            } else if (value?.constructor == String) {
              if (value?.length > rule.max) {
                error[field.key] = rule.msg || `${name}最多${rule.max}个字符`;
                continue;
              }
            } else if (value?.constructor == Array) {
              if (value?.length > rule.max) {
                error[field.key] = rule.msg || `${name}最多选择${rule.max}项`;
                continue;
              }
            }
          }

          const valid = rule.valid?.(value, model, field);
          if (valid)
            error[field.key] = rule.msg || valid;
        }
        return !error[field.key];
      }
      return true;
    },

    /** 验证所有字段 */
    validAll() {
      const form = this.form;
      let success = true;
      for (const field of form)
        success &= this.valid(field);
      return success;
    },

    async submit() {
      const success = this.validAll();
      if (!success)
        return;

      // this.modelValue = { ...this.cloneModel };
      // const over = !(await this.onSubmit?.(this.cloneModel, this.$el));
      await this.onSubmit?.(this.cloneModel, this.$el);
      // if (over)
      //   this.modelValue = undefined;
    },
    reset() {
      // this.cloneModel = JSON.parse(JSON.stringify(this.modelValue));
      this.cloneModel = {};
      this.error = {};
      this.onReset?.apply?.(this, this.cloneModel);
    },
    /** 取消编辑 */
    cancel() {
      this.onCancel?.(this.modelValue, this.$el);
      // if (!(await this.onCancel?.(this.modelValue, this.$el)))
      //   this.modelValue = undefined;
    },
  },
})
</script>

<style lang="scss" scoped>
.ee-form {
  --ee-form-item-label: 6em;
  --ee-form-item-align: left;
  color: var(--text1);
  display: flex;
  flex-direction: column;
  gap: 1.5em;

  // &.layout-left {
  //   --ee-form-item-align: left;
  // }

  &.layout-right {
    --ee-form-item-align: right;
  }

  &.layout-top {
    --ee-form-item-label: 100%;
  }

  &.layout-inline {
    flex-direction: row;
    flex-wrap: wrap;
    --ee-form-item-label: fit-content;
  }

  &>div {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 1em;

    &>div {
      flex: 1;
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: .5em;
      position: relative;

      &>* {
        flex: 1;
      }

      .error {
        position: absolute;
        top: 100%;
        margin: 0;
        font-size: .8125em;
        color: var(--danger0);
      }
    }

    &>label {
      min-width: var(--ee-form-item-label);
      max-width: var(--ee-form-item-label);
      text-align: var(--ee-form-item-align);

      &.required::before {
        content: '*';
        color: var(--danger0);
        margin-right: .25em;
      }

      &.colon::after {
        content: ':';
        margin-left: .25em;
      }
    }
  }
}
</style>